package com.s7.widget.widget;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.s7.widget.attr.WheelPickerAttr;
import com.s7.widget.utils.LinearGradient;

import java.text.Format;
import java.util.ArrayList;
import java.util.List;

/**
 * 滚轮选择器
 * @param <T>
 */
@SuppressWarnings({"FieldCanBeLocal", "unused", "SameParameterValue"})
public class WheelPicker<T> extends View {

    private List<T> mDatas;

    private Format mDataFormat;

    private WheelPickerAttr mAttr;
    @ColorInt
    private int mUnSelectedColor;
    private int mUnSelectedSize;
    private boolean isGradual;
    @ColorInt
    private int mSelectedColor;
    private int mSelectedSize;
    private Paint mPaint, mUnSelectedPaint, mSelectedPaint, mIndicatorPaint;
    private String mIndicator;
    @ColorInt
    private int mIndicatorColor;
    private int mIndicatorSize;
    private int mIndicatorDrawX = 0;
    private int mSpacing;
    private int mMaxWidth = 0, mMaxHeight = 0;
    private int mHalfCount;
    private int mItemHeight;
    private int mCurrent;
    private boolean isZoom;
    private boolean isCurtain;
    @ColorInt
    private int mCurtainColor;
    private boolean isCurtainBorder;
    @ColorInt
    private int mCurtainBorderColor;
    private Rect mDrawnRect;
    private Rect mSelectedRect;
    private int mFirstItemDrawX, mFirstItemDrawY;
    private int mCenterItemDrawnY;
    private Scroller mScroller;
    private int mTouchSlop;
    private boolean mTouchSlopFlag;
    private VelocityTracker mTracker;
    private int mTouchDownY;
    private int mScrollOffsetY;
    private int mLastDownY;
    private boolean isCyclic;
    private int mMaxFlingY, mMinFlingY;
    private int mMinimumVelocity = 50, mMaximumVelocity = 12000;
    private boolean mIsAbortScroller;

    private LinearGradient mLinearGradient;

    private Handler mHandler = new Handler();

    private OnWheelChangeListener<T> mListener;

    private Runnable mScrollerRunnable = new Runnable() {
        @Override
        public void run() {

            if (mScroller.computeScrollOffset()) {
                mScrollOffsetY = mScroller.getCurrY();
                postInvalidate();
                mHandler.postDelayed(this, 16);
            }
            if (mScroller.isFinished() || (mScroller.getFinalY() == mScroller.getCurrY()
                    && mScroller.getFinalX() == mScroller.getCurrX())) {

                if (mItemHeight == 0) {
                    return;
                }
                int position = -mScrollOffsetY / mItemHeight;
                position = fixItemPosition(position);
                if (mCurrent != position) {
                    mCurrent = position;
                    if (mListener != null) {
                        mListener.onWheelSelected(WheelPicker.this,
                                mDatas.get(position), position);
                    }
                }
            }
        }
    };

    public WheelPicker(Context context) {
        this(context, null);
    }

    public WheelPicker(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WheelPicker(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mAttr = new WheelPickerAttr(context, attrs, defStyleAttr);
        initAttrs();
        init(context);
    }

    private void init(Context context) {
        mDatas = new ArrayList<>();
        initPaint();
        mLinearGradient = new LinearGradient(mUnSelectedColor, mSelectedColor);
        mDrawnRect = new Rect();
        mSelectedRect = new Rect();
        mScroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();
    }

    private void initAttrs() {
        if (mAttr == null) {
            return;
        }
        mHalfCount = mAttr.getHalfCount();
        mCurrent = mAttr.getCurrent();
        mSelectedColor = mAttr.getSelectedColor();
        mSelectedSize = mAttr.getSelectedSize();
        mUnSelectedColor = mAttr.getUnSelectedColor();
        mUnSelectedSize =mAttr.getUnSelectedSize();
        isGradual = mAttr.isGradual();
        isZoom = mAttr.isZoom();
        isCyclic = mAttr.isCyclic();
        isCurtain = mAttr.isCurtain();
        mSpacing = mAttr.getSpacing();
        mCurtainColor = mAttr.getCurtainColor();
        isCurtainBorder = mAttr.isCurtainBorder();
        mCurtainBorderColor = mAttr.getCurtainBorderColor();
        mIndicator = mAttr.getIndicator();
        mIndicatorColor = mAttr.getIndicatorColor();
        mIndicatorSize = mAttr.getIndicatorSize();
    }

    public void getMaxHeight() {
        mPaint.setTextSize(mSelectedSize > mUnSelectedSize ? mSelectedSize : mUnSelectedSize);
        Paint.FontMetrics metrics = mPaint.getFontMetrics();
        mMaxHeight = (int) (metrics.bottom - metrics.top) + mSpacing;
    }

    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.CENTER);
        mUnSelectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
        mUnSelectedPaint.setStyle(Paint.Style.FILL);
        mUnSelectedPaint.setTextAlign(Paint.Align.CENTER);
        mUnSelectedPaint.setColor(mUnSelectedColor);
        mUnSelectedPaint.setTextSize(mUnSelectedSize);
        mSelectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
        mSelectedPaint.setStyle(Paint.Style.FILL);
        mSelectedPaint.setTextAlign(Paint.Align.CENTER);
        mSelectedPaint.setColor(mSelectedColor);
        mSelectedPaint.setTextSize(mSelectedSize);
        mIndicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
        mIndicatorPaint.setStyle(Paint.Style.FILL);
        mIndicatorPaint.setTextAlign(Paint.Align.CENTER);
        mIndicatorPaint.setColor(mIndicatorColor);
        mIndicatorPaint.setTextSize(mIndicatorSize);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        getMaxHeight();
        mMaxWidth = widthMeasureSpec;
        int height = mMaxHeight * getHalfCount() + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(mMaxWidth, height);
    }

    private void computeFlingLimitY() {
        if (mDatas == null) {
            mMinFlingY = isCyclic ? Integer.MIN_VALUE : - mItemHeight * ( - 1);
        } else {
            mMinFlingY = isCyclic ? Integer.MIN_VALUE : - mItemHeight * (mDatas.size() - 1);
        }
        mMaxFlingY = isCyclic ? Integer.MAX_VALUE : 0;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mDrawnRect.set(getPaddingLeft(), getPaddingTop(),
                getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
        mItemHeight = mDrawnRect.height() / getHalfCount();
        mFirstItemDrawX = mDrawnRect.centerX();
        if (!TextUtils.isEmpty(mIndicator)) {
            mIndicatorDrawX = (int) (mDrawnRect.right - mIndicatorPaint.measureText(mIndicator) / 2) - 10;
        }
        mFirstItemDrawY = (int) ((mItemHeight - (mSelectedPaint.ascent() + mSelectedPaint.descent())) / 2);
        mSelectedRect.set(getPaddingLeft(), getPaddingTop() + mItemHeight * mHalfCount,
                getWidth() - getPaddingRight(),
                getPaddingBottom() + mItemHeight + mItemHeight * mHalfCount);
        computeFlingLimitY();
        mCenterItemDrawnY = mFirstItemDrawY + mItemHeight * mHalfCount;
        mScrollOffsetY = - mItemHeight * mCurrent;
    }

    private int fixItemPosition(int position) {
        if (mDatas == null || mDatas.size() == 0) {
            position = 0;
            return position;
        }

        if (position < 0) {
            position = mDatas.size() + (position % mDatas.size());

        }
        if (position >= mDatas.size()){
            position = position % mDatas.size();
        }
        return position;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        getMaxHeight();
        computeFlingLimitY();
        mPaint.setTextAlign(Paint.Align.CENTER);
        drawCurtain(canvas);
        drawCurtainBorder(canvas);
        mPaint.setStyle(Paint.Style.FILL);
        if (mDatas != null && mDatas.size() > 0) {
            drawData(canvas);
        }
        drawIndicator(canvas);
    }

    private void drawIndicator(Canvas canvas) {
        if (!TextUtils.isEmpty(mIndicator)) {
            mIndicatorPaint.setColor(mIndicatorColor);
            mIndicatorPaint.setTextSize(mIndicatorSize);
            canvas.drawText(mIndicator, mIndicatorDrawX, mCenterItemDrawnY, mIndicatorPaint);
        }
    }

    private void drawData(Canvas canvas) {
        int drawnSelectedPos = - mScrollOffsetY / mItemHeight;
        int min = drawnSelectedPos - mHalfCount - 1;
        int max = drawnSelectedPos + mHalfCount + 1;
        for (int drawDataPos = min; drawDataPos <= max; drawDataPos++) {
            int position = drawDataPos;
            if (isCyclic) {
                position = fixItemPosition(position);
            } else {
                if (position < 0 || position > mDatas.size() - 1) {
                    continue;
                }
            }
            int itemDrawY = mFirstItemDrawY + (drawDataPos + mHalfCount) * mItemHeight + mScrollOffsetY;
            int distanceY = Math.abs(mCenterItemDrawnY - itemDrawY);
            initGradual(itemDrawY, distanceY);
            textZoom(distanceY);
            String drawText = getData(mDatas.get(position));
            if (distanceY < mItemHeight / 2) {
                canvas.drawText(drawText, mFirstItemDrawX, itemDrawY, mSelectedPaint);
            } else {
                canvas.drawText(drawText, mFirstItemDrawX, itemDrawY, mUnSelectedPaint);
            }
        }
    }

    public String getData(T data) {
        return mDataFormat == null ? data.toString() : mDataFormat.format(data);
    }

    public T getData() {
        if (mDatas != null && mDatas.size() > 0) {
            return mDatas.get(mCurrent);
        }
        Object o = null;
        return (T) o;
    }

    private void initGradual(int itemDrawY, int distanceY) {
        if (isGradual) {
            if (distanceY < mItemHeight) {
                float colorRatio = 1 - (distanceY / (float) mItemHeight);
                mSelectedPaint.setColor(mSelectedColor);
                mUnSelectedPaint.setColor(mLinearGradient.getColor(colorRatio));
            } else {
                mSelectedPaint.setColor(mSelectedColor);
                mUnSelectedPaint.setColor(mUnSelectedColor);
            }
            float alphaRatio;
            if (itemDrawY > mCenterItemDrawnY) {
                alphaRatio = (mDrawnRect.height() - itemDrawY) /
                        (float) (mDrawnRect.height() - (mCenterItemDrawnY));
            } else {
                alphaRatio = itemDrawY / (float) mCenterItemDrawnY;
            }

            alphaRatio = alphaRatio < 0 ? 0 :alphaRatio;
            mSelectedPaint.setAlpha((int) (alphaRatio * 255));
            mUnSelectedPaint.setAlpha((int) (alphaRatio * 255));
        }
    }


    private void textZoom(int distanceY) {
        mSelectedPaint.setTextSize(mSelectedSize);
        /**
         * 开启此选项,会将越靠近中心的Item字体放大
         */
        if (isZoom) {
            if (distanceY < mItemHeight) {
                float addedSize = (mItemHeight - distanceY) / (float) mItemHeight * (mSelectedSize - mUnSelectedSize);
                mUnSelectedPaint.setTextSize(mUnSelectedSize + addedSize);
            } else {
                mUnSelectedPaint.setTextSize(mUnSelectedSize);
            }
        } else {
            mUnSelectedPaint.setTextSize(mUnSelectedSize);
        }
    }

    private void drawCurtain(Canvas canvas) {
        if (isCurtain) {
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(mCurtainColor);
            canvas.drawRect(mSelectedRect, mPaint);
        }
    }

    private void drawCurtainBorder(Canvas canvas) {
        if (isCurtainBorder) {
            mPaint.setStrokeWidth(2);
            mPaint.setColor(mCurtainBorderColor);
            canvas.drawLine(mSelectedRect.left, mSelectedRect.top,
                    mSelectedRect.right, mSelectedRect.top, mPaint);
            canvas.drawLine(mSelectedRect.left, mSelectedRect.bottom,
                    mSelectedRect.right, mSelectedRect.bottom, mPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mDatas == null || mDatas.size() == 0) {
            return false;
        }
        if (mTracker == null) {
            mTracker = VelocityTracker.obtain();
        }
        mTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    mIsAbortScroller = true;
                } else {
                    mIsAbortScroller = false;
                }
                mTracker.clear();
                mTouchDownY = mLastDownY = (int) event.getY();
                mTouchSlopFlag = true;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mTouchSlopFlag && Math.abs(mTouchDownY - event.getY()) < mTouchSlop) {
                    break;
                }
                mTouchSlopFlag = false;
                float move = event.getY() - mLastDownY;
                mScrollOffsetY += move;
                mLastDownY = (int) event.getY();
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                if (!mIsAbortScroller && mTouchDownY == mLastDownY) {
                    performClick();
                    if (event.getY() > mSelectedRect.bottom) {
                        int scrollItem = (int) (event.getY() - mSelectedRect.bottom) / mItemHeight + 1;
                        mScroller.startScroll(0, mScrollOffsetY, 0,
                                -scrollItem * mItemHeight);

                    } else if (event.getY() < mSelectedRect.top) {
                        int scrollItem = (int) (mSelectedRect.top - event.getY()) / mItemHeight + 1;
                        mScroller.startScroll(0, mScrollOffsetY, 0,
                                scrollItem * mItemHeight);
                    }
                } else {
                    mTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int velocity = (int) mTracker.getYVelocity();
                    if (Math.abs(velocity) > mMinimumVelocity) {
                        mScroller.fling(0, mScrollOffsetY, 0, velocity,
                                0, 0, mMinFlingY, mMaxFlingY);
                        mScroller.setFinalY(mScroller.getFinalY() +
                                computeDistanceToEndPoint(mScroller.getFinalY() % mItemHeight));
                    } else {
                        mScroller.startScroll(0, mScrollOffsetY, 0,
                                computeDistanceToEndPoint(mScrollOffsetY % mItemHeight));
                    }
                }
                if (!isCyclic) {
                    if (mScroller.getFinalY() > mMaxFlingY) {
                        mScroller.setFinalY(mMaxFlingY);
                    } else if (mScroller.getFinalY() < mMinFlingY) {
                        mScroller.setFinalY(mMinFlingY);
                    }
                }
                mHandler.post(mScrollerRunnable);
                mTracker.recycle();
                mTracker = null;
                break;
        }
        return true;
    }

    @Override
    public boolean performClick() {
        return super.performClick();
    }

    private int computeDistanceToEndPoint(int remainder) {
        if (Math.abs(remainder) > mItemHeight / 2) {
            if (mScrollOffsetY < 0) {
                return -mItemHeight - remainder;
            } else {
                return mItemHeight - remainder;
            }
        } else {
            return -remainder;
        }
    }

    public void setOnWheelListener(OnWheelChangeListener<T> mListener) {
        this.mListener = mListener;
    }

    public List<T> getDatas() {
        return mDatas;
    }

    public void setData(@NonNull List<T> datas) {
        if (datas == null || datas.size() == 0) {
            return;
        }
        mDatas.clear();
        mDatas.addAll(datas);
        postInvalidate();
    }

    public void setData(@NonNull List<T> datas, int currentPosition) {
        if (datas == null || datas.size() == 0) {
            return;
        }
        if (currentPosition < 0) {
            currentPosition = 0;
        }
        if (currentPosition > datas.size() - 1) {
            currentPosition = datas.size() - 1;
        }
        mDatas.clear();
        mDatas.addAll(datas);
        setCurrentPosition(currentPosition);
        postInvalidate();
    }

    public void setUnSelectedColor(@ColorInt int textColor) {
        if (mUnSelectedColor == textColor) {
            return;
        }
        mUnSelectedColor = textColor;
        mLinearGradient.setStartColor(textColor);
        postInvalidate();
    }

    public void setUnselectedSize(int textSize) {
        if (mUnSelectedSize == textSize) {
            return;
        }
        mUnSelectedSize = textSize;
        getMaxHeight();
        postInvalidate();
    }

    public void setSelectedColor(@ColorInt int selectedColor) {
        if (mSelectedColor == selectedColor) {
            return;
        }
        mSelectedColor = selectedColor;
        mLinearGradient.setEndColor(selectedColor);
        postInvalidate();
    }

    public void setSelectedSize(int selectedSize) {
        if (mSelectedSize == selectedSize) {
            return;
        }
        mSelectedSize = selectedSize;
        getMaxHeight();
        postInvalidate();
    }

    public int getHalfCount() {
        return mHalfCount * 2 + 1;
    }

    public void setHalfCount(int count) {
        if (this.mHalfCount == count) {
            return;
        }
        this.mHalfCount = count;
        requestLayout();
    }

    public void setCurrentPosition(int currentPosition) {
        setCurrentPosition(currentPosition, true);
    }

    public synchronized void setCurrentPosition(int currentPosition, boolean smoothScroll) {
        if (mDatas != null && mDatas.size() > 0 && currentPosition > mDatas.size() - 1) {
            currentPosition = mDatas.size() - 1;
        }
        if (currentPosition < 0) {
            currentPosition = 0;
        }
        if (mCurrent == currentPosition) {
            return;
        }
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }
        if (smoothScroll && mItemHeight > 0) {
            mScroller.startScroll(0, mScrollOffsetY, 0, (mCurrent - currentPosition) * mItemHeight);
            int finalY = -currentPosition * mItemHeight;
            mScroller.setFinalY(finalY);
            mHandler.post(mScrollerRunnable);
        } else {
            mCurrent = currentPosition;
            mScrollOffsetY = - mItemHeight * mCurrent;
            postInvalidate();
            if (mListener != null) {
                mListener.onWheelSelected(this, mDatas.get(currentPosition), currentPosition);
            }
        }
    }

    public void setZoom(boolean zoom) {
        if (isZoom == zoom) {
            return;
        }
        isZoom = zoom;
        postInvalidate();
    }

    public void setCyclic(boolean cyclic) {
        if (isCyclic == cyclic) {
            return;
        }
        isCyclic = cyclic;
        computeFlingLimitY();
        requestLayout();
    }

    public void setMinimumVelocity(int minimumVelocity) {
        mMinimumVelocity = minimumVelocity;
    }

    public void setMaximumVelocity(int maximumVelocity) {
        mMaximumVelocity = maximumVelocity;
    }

    public void setGradual(boolean gradual) {
        if (isGradual == gradual) {
            return;
        }
        isGradual = gradual;
        postInvalidate();
    }

    public void setCurtain(boolean isCurtain) {
        if (this.isCurtain == isCurtain) {
            return;
        }
        this.isCurtain = isCurtain;
        postInvalidate();
    }

    public void setCurtainColor(@ColorInt int curtainColor) {
        if (mCurtainColor == curtainColor) {
            return;
        }
        mCurtainColor = curtainColor;
        postInvalidate();
    }

    public void setCurtainBorder(boolean showCurtainBorder) {
        if (isCurtainBorder == showCurtainBorder) {
            return;
        }
        isCurtainBorder = showCurtainBorder;
        postInvalidate();
    }

    public void setCurtainBorderColor(@ColorInt int curtainBorderColor) {
        if (mCurtainBorderColor == curtainBorderColor) {
            return;
        }
        mCurtainBorderColor = curtainBorderColor;
        postInvalidate();
    }

    public void setIndicator(String indicator) {
        mIndicator = indicator;
        requestLayout();
        postInvalidate();
    }

    public void setIndicatorColor(int indicatorColor) {
        mIndicatorColor = indicatorColor;
        postInvalidate();
    }

    public void setIndicatorSize(int indicatorSize) {
        mIndicatorSize = indicatorSize;
        requestLayout();
        postInvalidate();
    }

    public void setDataFormat(Format dataFormat) {
        mDataFormat = dataFormat;
        postInvalidate();
    }

    public interface OnWheelChangeListener<T> {
        void onWheelSelected(WheelPicker picker, T data, int position);
    }
}
